package com.rafali.flickruploader.tool; import com.google.common.base.Joiner; import com.rafali.common.AndroidRpcInterface; import com.rafali.common.ToolString; import com.rafali.flickruploader.Config; import com.rafali.flickruploader.FlickrUploader; import com.squareup.okhttp.Call; import com.squareup.okhttp.Interceptor; import com.squareup.okhttp.MediaType; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; import com.squareup.okhttp.RequestBody; import com.squareup.okhttp.Response; import org.androidannotations.api.BackgroundExecutor; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InterruptedIOException; import java.io.StreamCorruptedException; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.net.SocketException; import java.net.UnknownHostException; import java.util.Arrays; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import javax.net.ssl.SSLHandshakeException; public final class RPC { private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(RPC.class); private static final int DEFAULT_MAX_RETRIES = 5; private static final int RETRY_SLEEP_TIME_MILLIS = 2000; public static class UserAgentInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { Request originalRequest = chain.request(); Request.Builder builder = originalRequest.newBuilder() .removeHeader("User-Agent") .addHeader("User-Agent", "rafali-android-rpc gzip") .addHeader("rafali-versionCode", "" + Config.VERSION) .addHeader("rafali-packageName", FlickrUploader.getAppContext().getPackageName()) .addHeader("Content-Type", "application/json;charset=UTF-8"); Request requestWithUserAgent = builder.build(); return chain.proceed(requestWithUserAgent); } } static final OkHttpClient client = new OkHttpClient(); static { client.networkInterceptors().add(new UserAgentInterceptor()); } private static String postRpc(Method method, Object[] args) { String responseStr = null; boolean retry = true; int executionCount = 0; while (retry) { try { if (executionCount > 0) { Thread.sleep(RETRY_SLEEP_TIME_MILLIS * executionCount); } executionCount++; Request.Builder builder = new Request.Builder().url(Config.HTTP_START + "/androidRpc?method=" + method.getName()); if (args != null) { // String encode = ToolStream.encode(args); // LOG.debug("encoded string : " + encode.length()); // LOG.debug("gzipped string : " + ToolStream.encodeGzip(args).length()); Object[] args_logs = new Object[3]; args_logs[0] = args; builder.post(RequestBody.create(MediaType.parse("*/*"), Streams.encodeGzip(args_logs))); } else { builder.post(RequestBody.create(MediaType.parse("*/*"), "")); } LOG.info("postRpc " + method.getName() + "(" + Joiner.on(',').useForNull("null").join(args) + ")"); Request request = builder.build(); Call call = client.newCall(request); Response response = call.execute(); responseStr = response.body().string(); retry = false; } catch (Throwable e) { LOG.error("Failed androidRpc (" + e.getClass().getCanonicalName() + ") executionCount=" + executionCount + " : " + method.getName() + " : " + Arrays.toString(args)); if (e instanceof InterruptedIOException || e instanceof SSLHandshakeException) { retry = false; } else if (executionCount >= DEFAULT_MAX_RETRIES) { retry = false; } if (e instanceof UnknownHostException || e instanceof SocketException) { notifyNetworkError(); } } } return responseStr; } private static long lastNotifyNetworkError = 0; static void notifyNetworkError() { if (System.currentTimeMillis() - lastNotifyNetworkError > 10000) { lastNotifyNetworkError = System.currentTimeMillis(); Utils.toast("Network error, retrying…"); } } private static final AndroidRpcInterface rpcService = (AndroidRpcInterface) Proxy.newProxyInstance(RpcHandler.class.getClassLoader(), new Class<?>[]{AndroidRpcInterface.class}, new RpcHandler()); public static AndroidRpcInterface getRpcService() { return rpcService; } private static class RpcHandler implements InvocationHandler { // HTTP response should stay in cache for 5 seconds private static final int HTTP_CACHE_TIMEOUT = 5000; @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // LOG.trace("method : " + method); if (args == null) { args = new Object[0]; } final String key = method.getName() + "-" + getKey(args); Object lock = locks.get(key); long start = System.currentTimeMillis(); if (method.getReturnType() != void.class && lock != null && !responses.containsKey(key)) { synchronized (lock) { do { try { LOG.info("###### concurrent RPC call " + method.getReturnType().getSimpleName() + " " + method.getName() + "(" + Arrays.toString(args) + ")"); lock.wait(2 * HTTP_CACHE_TIMEOUT); } catch (InterruptedException e) { } } while (!responses.containsKey(key) && System.currentTimeMillis() - start < HTTP_CACHE_TIMEOUT); } } else { lock = new Object(); locks.put(key, lock); } try { Object object = null; String response; if (responses.containsKey(key)) { LOG.info("###### response still in cache : " + key + ", after " + ToolString.formatDuration(System.currentTimeMillis() - start)); object = responses.get(key); } else { int retry = 0; while (retry < DEFAULT_MAX_RETRIES) { retry++; response = postRpc(method, args); if (ToolString.isBlank(response)) { break; } else { object = Streams.decode(response); if (object instanceof Throwable) { LOG.warn("retry:" + retry + ", server exception :" + object.getClass().getName() + "," + ((Throwable) object).getMessage()); } else { break; } } } if (object != null) { responses.put(key, object); BackgroundExecutor.execute(new Runnable() { @Override public void run() { responses.remove(key); } }, HTTP_CACHE_TIMEOUT); } } if (object instanceof Throwable) { if (object instanceof StreamCorruptedException || object instanceof IOException) { LOG.warn(object.getClass().getSimpleName() + " on " + method.getName()); } else { LOG.error("Server error calling " + method.getName(), (Throwable) object); } return null; // throw new Exception(throwable.getClass().getSimpleName() + " : " + throwable.getMessage()); } return object; } finally { synchronized (lock) { lock.notifyAll(); locks.remove(key); } } } private Map<String, Object> responses = new ConcurrentHashMap<>(); private Map<String, Object> locks = new ConcurrentHashMap<>(); private String getKey(Object[] args) { String key = ""; for (Object object : args) { key += object != null ? object.hashCode() : ""; } return key; } } }